--[[
Datastream Module
By Janorkie and Deco da Man
]]--

require "glon"
AddCSLuaFile "includes/modules/datastream.lua"
AddCSLuaFile "includes/modules/glon.lua"

function string.split(str,d)
	local t = {}
	local len = str:len()
	local i = 0
	while i*d < len do
		t[i+1] = str:sub(i*d+1,(i+1)*d)
		i=i+1
	end
	return t
end

if SERVER then

local table = table
local string = string
local umsg = umsg
local concommand = concommand
local util = util
local hook = hook
local math = math
local type = type
local pairs = pairs
local tostring = tostring
local tonumber = tonumber
local RecipientFilter = RecipientFilter
local pcall = pcall
local error = error
local glon = glon
local PCMod = PCMod or { GM = GAMEMODE }

module "datastream"
local _outgoing = {}
local _incoming = {}
local _hooks = {}

function Hook(h,f)
	local hk = {}
	hk.handler = h
	hk.func = f
	_hooks[h] = hk
end

function StreamToClients( rcp, h, d, cb )
	local o = {}
	// Recipient information
	local rt = type(rcp)
	if rt == "RecipientFilter" then
		o.rf = rcp
	elseif rt == "table" then
		o.rf = RecipientFilter()
		for k,v in pairs(rcp) do
			o.rf:AddPlayer(v)
		end
	elseif rt == "Player" then
		o.rf = rcp
	else
		error(("Invalid type %q given to datastream.StreamToClients for recipients!"):format(rcp))
	end
	// Data information
	o.decdata = d
	o.encdata = ""
	o.sent = 0
	o.size = 0
	// Operation state
	o.block = 1
	o.state = 1
	// Operation handlers
	o.handler = h
	o.callback = cb
	// Put the operation into the queue and finish up
	o.id = table.insert(_outgoing,o)
	return o.id
end

function DownstreamActive() return #_incoming > 0 end

function GetProgress(id)
	if _outgoing[id] then return _outgoing[id].sent end
end

local function CallStreamHook(pl,h,id,encdat,decdat)
	if not _hooks[h] then
		error("DataStreamServer: Unhandled stream "..h.."!")
	return end
	if hook.Call("CompletedIncomingStream",PCMod.GM,pl,h,id,encdat,decdat) then return end
	_hooks[h].func(pl,h,id,encdat,decdat)
end

local function StreamTick()
	for id,o in pairs(_outgoing) do
		if o.state == 1 then // Processing stage.
			if o.decdata then
				local enc = ""
				local b,err = pcall(glon.encode, o.decdata)
				if b then enc=err else error("DataStreamServer Encoding Error: "..err.." (operation "..o.id..")") end
				o.encdata = string.split(enc,128)
				o.state = 2
			else
				o.state = 3
			end
			umsg.Start("DSStart",o.rf)
				umsg.Long(o.id)
				umsg.String(o.handler)
			umsg.End()
			break
		elseif o.state == 2 then // Sending stage.
			umsg.Start("DSPacket",o.rf)
				umsg.Long(o.id)
				umsg.String(o.encdata[o.block])
			umsg.End()
			o.block = o.block+1
			if o.block > #o.encdata then o.state = 3 end
			break
		elseif o.state == 3 then // Ending stage
			umsg.Start("DSEnd",o.rf)
				umsg.Long(o.id)
			umsg.End()
			if o.callback then o.callback(o.id) end
			_outgoing[o.id] = nil
			break
		end
	end
end
hook.Add("Tick","DatastreamTick",StreamTick)

// Default stuff that will be put in base gamemode
local function AcceptStream()
	return true
end
hook.Add("AcceptStream","AcceptStream",AcceptStream)

// Messages from the client
local function DSRequest(pl,cmd,args)
	if args[1] ~= "\1" then return end
	local h = args[2]
	local id = tonumber(args[3])
	if hook.Call("AcceptStream",PCMod.GM,pl,h,id) then
		local o = {
			pl = pl,
			handler = h,
			buffer = "",
		}
		o.id = table.insert(_incoming,o)
		umsg.Start("DSRequestOK",pl)
			umsg.Long(id)
			umsg.Long(o.id)
		umsg.End()
	else
		umsg.Start("DSRequestDenied",pl)
			umsg.Long(id)
		umsg.End()
	end
end
concommand.Add("__dsr",DSRequest)

local function DSPacket(pl,cmd,args)
	if args[1] ~= "\1" then return end
	local id = tonumber(args[3])
	if not _incoming[id] then return end
	local data = args[2]
	_incoming[id].buffer = _incoming[id].buffer..data
	umsg.Start("DSPacketReceived",pl)
		umsg.Long(id)
		umsg.Long(_incoming[id].buffer:len())
	umsg.End()
end
concommand.Add("__dsp",DSPacket)

local function DSEnd(pl,cmd,args)
	if args[1] ~= "\1" then return end
	local id = tonumber(args[2])
	if not _incoming[id] then return end
	local reptab = {["\\"]="\\",["n"]="\n"}
	local encdat = string.gsub(_incoming[id].buffer,"\\(.)",reptab)
	local decdat = ""
	local b,err = pcall(glon.decode,encdat)
	if b then decdat=err else error("DataStreamServer Decoding Error: "..err.." (operation "..id..")") end
	CallStreamHook(pl,_incoming[id].handler,id,encdat,decdat)
	_incoming[id] = nil;
	umsg.Start("DSConfirmEnd",pl)
		umsg.Long(id)
	umsg.End()
end
concommand.Add("__dse",DSEnd)

elseif CLIENT then
local table = table
local string = string
local usermessage = usermessage
local hook = hook
local math = math
local os = os
local util = util
local RunConsoleCommand = RunConsoleCommand
local LocalPlayer = LocalPlayer
local type = type
local pairs = pairs
local tostring = tostring
local RecipientFilter = RecipientFilter
local pcall = pcall
local error = error
local glon = glon

module "datastream"

local _outgoing = {}
local _incoming = {}
local _tempout = {}
local _hooks = {}

function Hook(h,f)
	local hk = {}
	hk.handler = h
	hk.func = f
	_hooks[h] = hk
end

function StreamToServer(h,d,cb,acb)
	local o = {
		// Data information
		decdata = d,
		encdata = "",
		sent = 0,
		size = 0,
		// Operation state
		block = 1,
		state = 1,
		// Operation handlers
		handler = h,
		callback = cb,
		acallback = acb,
	}
	o.id = table.insert(_tempout,o)
	RunConsoleCommand("__dsr","\1",h,o.id)
	return o.id
end

function DownstreamActive() return #_incoming > 0 end

function GetProgress(id)
	if _outgoing[id] then return _outgoing[id].sent end
end

local function CallStreamHook(h,id,encdat,decdat)
	if not _hooks[h] then
		error("DataStreamClient: Unhandled stream "..h.."!")
	return end
	hook.Call("CompletedIncomingStream",PCMod.GM,h,id,encdat,decdat)
	_hooks[h].func(h,id,encdat,decdat)
end

local function StreamTick()
	for id,o in pairs(_outgoing) do
		if o.state == 1 then // Processing stage.
			if o.decdata then
				local enc = ""
				local b,err = pcall(glon.encode, o.decdata)
				if b then enc=err else error("DataStreamClient Encoding Error: "..err.." (operation "..o.id..")") end
				o.encdata = string.split(string.gsub(string.gsub(enc,"\\","\\\\"),"\n","\\n"),128)
				o.state = 2
			else
				o.state = 3
			end
			break
		elseif o.state == 2 then // Sending stage.
			RunConsoleCommand("__dsp","\1",o.encdata[o.block],o.id)
			o.block = o.block+1
			if o.block > #o.encdata then o.state = 3 end
			break
		elseif o.state == 3 then // Ending stage
			RunConsoleCommand("__dse","\1",o.id)
			o.state = 4
		end
	end
end
hook.Add("Tick","DatastreamTick",StreamTick)

// Usermessages from the server
local function DSStart(data)
	local id = data:ReadLong()
	local handler = data:ReadString()
	local o = {
	id = id,
	handler = handler,
	buffer = "",
	}
	_incoming[id] = o
end
usermessage.Hook("DSStart",DSStart)

local function DSPacket(data)
	local id = data:ReadLong()
	if not _incoming[id] then return end
	local data = data:ReadString()
	_incoming[id].buffer = _incoming[id].buffer..data
end
usermessage.Hook("DSPacket",DSPacket)

local function DSEnd(data)
	local id = data:ReadLong()
	if not _incoming[id] then return end
	local encdat = _incoming[id].buffer
	local decdat = ""
	local b,err = pcall(glon.decode,encdat)
	if b then decdat=err else error("DataStreamClient Decoding Error: "..err.." (operation "..id..")") end
	CallStreamHook(_incoming[id].handler,id,encdat,decdat)
	_incoming[id] = nil
end
usermessage.Hook("DSEnd",DSEnd)

local function DSConfirmEnd(data)
	local id = data:ReadLong()
	local o = _outgoing[id]
	if not o then return end
	if o.callback then
		o.callback(o.id)
	end
	_outgoing[o.id] = nil
end
usermessage.Hook("DSConfirmEnd",DSConfirmEnd)

// Accept/Deny Handlers
local function DSRequestOK(data)
	local tempid = data:ReadLong()
	local o = _tempout[tempid]
	if !o then return end
	_tempout[tempid] = nil;
	o.id = data:ReadLong()
	_outgoing[o.id] = o
	if o.acallback then
		o.acallback(true,tempid,o.id)
	end
end
usermessage.Hook("DSRequestOK",DSRequestOK)

local function DSRequestDenied(data)
	local tempid = data:ReadLong()
	local o = _tempout[tempid]
	if not o then return end
	_tempout[tempid] = nil
	if o.acallback then
		o.acallback(false, tempid)
	end
end
usermessage.Hook("DSRequestDenied",DSRequestDenied)

// Packet Confirmation Message
local function DSPacketReceived(data)
	local id = data:ReadLong()
	if not _outgoing[id] then return end
	_outgoing[id].sent = data:ReadLong()
end
usermessage.Hook("DSPacketReceived",DSPacketReceived)

end